﻿using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Windows;

namespace DG.Common.EF.Logging
{
    public class EFLogger<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>
        where TLogId : struct  , IComparable<TLogId>
        where TLogCampoId : struct  , IComparable<TLogCampoId>
        where TLogKeyId : struct  , IComparable<TLogKeyId>
        where TUsuarioID : struct  , IComparable<TUsuarioID>
        where TLogTipoId : struct  , IComparable
    {
        private readonly HashSet<Type> _entitiesLogFilter = new HashSet<Type>(
            new[]
            {
                typeof (ILog<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>),
                typeof (ILogCampo<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>),
                typeof (ILogKey<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>)
            });

        public string UsuarioNome { get; private set; }
        public TUsuarioID UsuarioID { get; private set; }
        private IEFLoggable<TLogId, TLogCampoId, TLogKeyId, TUsuarioID,TLogTipoId> Context { get; set; }

        public EFLogger(IEFLoggable<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId> context)
        {
            this.UsuarioNome = context.UsuarioNome;
            this.UsuarioID = context.UsuarioID;
            this.Context = context;
        }

        public int SaveChangesWithLogs()
        {
            Context.Context.ChangeTracker.DetectChanges();

            //using (var transaction = new TransactionScope())
            {
                var objCtx = ((IObjectContextAdapter) this.Context.Context).ObjectContext;

                var osEntryList =
                    objCtx.ObjectStateManager.GetObjectStateEntries(EntityState.Added
                                                                    | EntityState.Modified
                                                                    | EntityState.Deleted)
                        .ToList();
                var hora = DateTime.Now;
                var logs =
                    new Dictionary<ILog<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>, ObjectStateEntry>();
                var duplicatesLogsChecks =
                    new Dictionary
                        <ILog<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>,
                            Dictionary<ILogCampo<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>, string>>();
                Action
                    <ILog<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>,
                        ILogCampo<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>> checkLog = (log, logCampo) =>
                        {
                            if (!duplicatesLogsChecks.ContainsKey(log))
                                duplicatesLogsChecks.Add(log,
                                    new Dictionary
                                        <ILogCampo<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>, string>());
                            if (!duplicatesLogsChecks[log].ContainsKey(logCampo))
                            {
                                duplicatesLogsChecks[log].Add(logCampo, logCampo.Campo);
                            }
                        };
                foreach (var entry in osEntryList)
                {
                    var skip = false;
                    if (!entry.IsRelationship)
                    {
                        if (entry.Entity == null) throw new ArgumentNullException();
                        var entryType = new[] {entry.Entity.GetType(), entry.Entity.GetType().BaseType};

                        skip = _entitiesLogFilter.Any(entryType.Contains);
                    }

                    if (skip) continue;
                    if (entry.IsRelationship) continue;

                    ILog<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId> log;
                    ILogCampo<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId> logCampo;
                    switch (entry.State)
                    {
                        case EntityState.Added:
                        {
                            #region Added

                            log = Context.CreateLog();
                            log.LogTipoId = Context.AddedId;

                            var currents = entry.CurrentValues.DataRecordInfo.FieldMetadata.AsEnumerable().ToArray();
                            foreach (var p in currents)
                            {
                                var valorNovo = entry.CurrentValues.GetValue(p.Ordinal);

                                if (
                                    !(valorNovo == null || valorNovo == DBNull.Value ||
                                      (valorNovo is string && (String) valorNovo == "")))
                                {
                                    logCampo = Context.CreateLogItem();
                                    logCampo.Log = log;
                                    logCampo.Campo = p.FieldType.Name;
                                    checkLog(log, logCampo);

                                    logCampo.ValorNovo = (valorNovo is EntityKey)
                                        ? (((EntityKey) valorNovo).EntityKeyValues == null
                                            ? "Temporary: " + ((EntityKey) valorNovo).EntitySetName
                                            : string.Join(";",
                                                ((EntityKey) valorNovo).EntityKeyValues.Select(
                                                    e => string.Format("{0}: {1}", e.Key, e.Value))))
                                        : valorNovo.ToString();

                                    log.AddLogCampo(logCampo);
                                }
                            }

                            logs.Add(log, entry);
                            break;

                            #endregion
                        }
                        case EntityState.Modified:
                        {
                            #region Modified

                            var current = entry.CurrentValues;
                            var original = entry.OriginalValues;

                            log = Context.CreateLog();
                            log.LogTipoId = Context.ModifiedId;

                            foreach (var propertyName in entry.GetModifiedProperties())
                            {
                                var oldValue = original.GetValue(
                                    original.GetOrdinal(propertyName))
                                    .ToString();

                                var newValue = current.GetValue(
                                    current.GetOrdinal(propertyName))
                                    .ToString();

                                if (oldValue != newValue)
                                {
                                    logCampo = Context.CreateLogItem();
                                    logCampo.Log = log;
                                    logCampo.Campo = propertyName;
                                    checkLog(log, logCampo);

                                    logCampo.ValorAntigo = oldValue;
                                    logCampo.ValorNovo = newValue;
                                    log.AddLogCampo(logCampo);
                                }
                            }
                            if (log.GetLogCampos().Any())
                            {
                                logs.Add(log, entry);
                            }
                            break;

                            #endregion
                        }
                        case EntityState.Deleted:
                        {
                            #region Deleted

                            log = Context.CreateLog();

                            log.LogTipoId = Context.DeletedId;

                            if (!entry.IsRelationship)
                            {
                                foreach (
                                    var p in
                                        entry.GetUpdatableOriginalValues().DataRecordInfo.FieldMetadata.AsEnumerable())
                                {
                                    var valorAntigo = entry.OriginalValues.GetValue(p.Ordinal);
                                    if (
                                        !(valorAntigo == null || valorAntigo == DBNull.Value ||
                                          (valorAntigo is string && (String) valorAntigo == "")))
                                    {
                                        logCampo = Context.CreateLogItem();
                                        logCampo.Log = log;
                                        logCampo.Campo = p.FieldType.Name;
                                        checkLog(log, logCampo);

                                        logCampo.ValorAntigo = valorAntigo.ToString();
                                        log.AddLogCampo(logCampo);

                                    }
                                }
                            }
                            else
                            {
                                for (var i = 0; i < entry.OriginalValues.FieldCount; i++)
                                {
                                    var colName = entry.OriginalValues.GetName(i);
                                    logCampo = Context.CreateLogItem();
                                    logCampo.Log = log;
                                    logCampo.Campo = colName;
                                    checkLog(log, logCampo);

                                    log.AddLogCampo(logCampo);

                                    var entityKey = (EntityKey) entry.OriginalValues.GetValue(i);

                                    logCampo.ValorAntigo = string.Join(";",
                                        entityKey.EntityKeyValues.Select(e => string.Format("{0}: {1}", e.Key, e.Value)));
                                }
                            }
                            logs.Add(log, entry);

                            break;

                            #endregion
                        }
                    }
                }

                var maxId = Context.GetNextLogID();

                #region Fill Log With User Infos

                string version, pcname, ip;
                var user = LogExtensions.FillInfosToLog(out version, out pcname, out ip);

                var logsToAdd = new List<ILog<TLogId, TLogCampoId, TLogKeyId, TUsuarioID, TLogTipoId>>();

                foreach (var item in logs)
                {
                    maxId = Context.GetIncrementId(maxId);

                    var l = item.Key;
                    l.LogId = maxId;
                    l.Tabela = item.Value.EntitySet.Name;
                    l.Hora = hora;
                    l.UsuarioId = UsuarioID;
                    l.UsuarioNome = UsuarioNome;

                    l.MachineName = pcname;
                    l.WindowsUserName = user;
                    l.OperatingSystem = version;
                    l.IP = ip;

                    if (!item.Value.IsRelationship && item.Key.LogTipoId.CompareTo(Context.AddedId) != 0)
                        // diferente de inserção
                    {
                        foreach (var key in item.Value.EntityKey.EntityKeyValues)
                        {
                            var logKeys = Context.CreateLogKey();
                            item.Key.AddLogKey(logKeys);

                            logKeys.Campo = key.Key;
                            logKeys.Valor = Convert.ToString(key.Value);
                        }
                    }

                    logsToAdd.Add(l);
                }

                #endregion

                if (logsToAdd.Any())
                {
                    var logDbSet = Context.Context.Set(logsToAdd.First().GetType());

                    foreach (var log in logsToAdd)
                    {
                        logDbSet.Add(log);
                    }
                }

                var ret = Context.BaseSaveChanges();

                var extraLogUpdate = new List<string>();

                var nonRelationshipLogs =
                    logs.Where(e => e.Value.State != EntityState.Detached && !e.Value.IsRelationship &&
                                    e.Key.LogTipoId.CompareTo(Context.AddedId) == 0).ToArray();

                try
                {
                    //Adiciona as keys quando da inserção
                    foreach (var item in nonRelationshipLogs)
                    {
                        foreach (var key in item.Value.EntityKey.EntityKeyValues)
                        {
                            var logKeys = Context.CreateLogKey();
                            item.Key.AddLogKey(logKeys);

                            logKeys.Campo = key.Key;
                            logKeys.Valor = Convert.ToString(key.Value);
                        }
                    }

                    Context.BaseSaveChanges();
                }
                catch (Exception)
                {
                    extraLogUpdate.Add(
                        "Os dados foram gravados, mas não foi possível gerar o log com informações da inserção");
                }

                try
                {
                    //Checa ser os campos no BD estão OK, quando da inserção
                    foreach (var item in nonRelationshipLogs)
                    {
                        Context.Context.Entry(item.Value.Entity).Reload();
                        foreach (var p in item.Key.GetLogCampos())
                        {
                            var val =
                                item.Value.OriginalValues[item.Value.OriginalValues.GetOrdinal(p.Campo)].ToString();
                            //if (p.ValorNovo != val)
                            //{
                            p.ValorNovo = val;
                            //}
                        }
                    }

                    Context.BaseSaveChanges();
                }
                catch (Exception)
                {
                    extraLogUpdate.Add(
                        "Os dados foram gravados, mas não foi possível gerar o log com todas as informações corretas");
                }

                if (extraLogUpdate.Any())
                {
                    //MessageBox.Show(string.Join(Environment.NewLine, extraLogUpdate));
                }
                //transaction.Complete();
                return ret;
            }
        }
    }
}